4.2 Go语言的链接与加载

Go语言中,链接与加载是将编译后的多个目标文件组合成一个可执行文件,并最终在运行时加载该文件到内存中执行的过程。

在上节我们提到了汇编与链接,本节我们将详细探讨Go的链接与加载机制。

本节代码存放目录为 lesson11

链接过程

链接是将多个编译单元(目标文件)组合成一个单独的可执行文件的过程。在Go语言中,链接器(Linker)负责完成这一任务。

简单来说,就是当我们编译时,每个Go文件都会形成一个编译结果,链接就是将所有这些编译结果组合在一起的过程。

链接器在进行工作时,它的主要任务如下所示:

  • 符号解析:链接器负责解析符号引用,确保每个引用都能找到正确的定义。例如,函数调用、全局变量的引用都需要通过符号解析来确定它们的实际位置。

  • 地址重定位:在链接时,链接器会调整符号的地址,确保每个符号在最终的可执行文件中有正确的内存地址。

  • 合并和优化:链接器将各个编译单元的代码和数据段合并,进行全局优化。例如:删除未使用的代码。


Go语言中链接器具有以下特点:

  • 静态链接Go默认使用静态链接,生成的可执行文件包含所有依赖的库,这使得部署更加方便。

  • 增量编译:支持增量编译,减少了大型项目中的编译时间。

  • 内置Cgo支持Go的链接器能够处理Cgo代码,将C库链接到Go的可执行文件中。


链接的步骤如下所示:

  • 将中间代码转换为汇编代码。

  • 将汇编代码转换为机器指令。

  • 将多个转换结果进行链接,形成最终的可执行文件。

可执行文件

生成可执行文件后,操作系统将加载该文件并将其映射到内存中。典型的内存布局如下:

  • 代码段(.text):存放程序的指令。

  • 数据段(.data):存放已初始化的全局变量和静态变量。

  • 未初始化数据段(.bss):存放未初始化的全局变量和静态变量。

  • 堆(heap):用于动态内存分配。

  • 栈(stack):用于存放函数调用的局部变量、返回地址等。

接下来我们通过一个示例查看其最终的汇编实现,代码如下所示:

var (
    globalVar = "I am a global variable"
)

func main() {
    localVar := "I am a local variable"
    fmt.Println(globalVar)
    fmt.Println(localVar)
}

执行下面的命令进行编译:

go build -o example lesson11.go

编译完成后,会形成一个名为example的可执行文件,我们执行下面的命令查看汇编实现:

go tool objdump -s main example

输出如下所示:

TEXT main.main(SB) /Users/xc/xcWork/YouCanGo/GoDeeper/GoDeeperCode/lesson11/lesson11.go
  lesson11.go:9         0x100089f00             f9400b90                MOVD 16(R28), R16                       
  lesson11.go:9         0x100089f04             eb3063ff                CMP R16, RSP                            
  lesson11.go:9         0x100089f08             54000589                BLS 44(PC)                              
  lesson11.go:9         0x100089f0c             f81a0ffe                MOVD.W R30, -96(RSP)                    
  lesson11.go:9         0x100089f10             f81f83fd                MOVD R29, -8(RSP)                       
  lesson11.go:9         0x100089f14             d10023fd                SUB $8, RSP, R29                        
  lesson11.go:11        0x100089f18             a904ffff                STP (ZR, ZR), 72(RSP)                   
  lesson11.go:11        0x100089f1c             90000562                ADRP 704512(PC), R2                     
  lesson11.go:11        0x100089f20             9101c042                ADD $112, R2, R2                        
  lesson11.go:11        0x100089f24             f9400040                MOVD (R2), R0                           
  lesson11.go:11        0x100089f28             f9400441                MOVD 8(R2), R1                          
  lesson11.go:11        0x100089f2c             97fdfa75                CALL runtime.convTstring(SB)            
  lesson11.go:11        0x100089f30             d0000142                ADRP 172032(PC), R2                     
  lesson11.go:11        0x100089f34             912a8042                ADD $2720, R2, R2                       
  lesson11.go:11        0x100089f38             f90027e2                MOVD R2, 72(RSP)                        
  lesson11.go:11        0x100089f3c             f9002be0                MOVD R0, 80(RSP)

从输出结果中我们可以看到汇编指令,通过上面的输出我们可以看到一个大概的情况。


加载过程是操作系统将可执行文件加载到内存中,并将其准备好执行的过程。主要步骤如下所示:

  • 加载可执行文件:操作系统将可执行文件加载到内存中,并创建一个进程。

  • 动态链接库的加载:如果程序依赖动态库,操作系统会加载这些库,并将它们映射到进程的地址空间中。

  • 执行入口代码:操作系统将控制权交给可执行文件,程序从main函数开始执行。

小结

Go语言的链接和加载过程是将源代码转换为可执行文件的关键步骤。通过理解链接器的工作原理、内存布局和加载过程,我们可以更好地优化和调试Go程序。

关于本节总结如下:

  • 通过链接将多个目标文件最终合并为一个可执行文件

  • 程序启动时系统加载可执行文件到内存代码段中

results matching ""

    No results matching ""